#!/usr/bin/env python
#
# Stoker: an LDAP-aware utility for cluster/networked system management
#
# Author:   Mike Murphy (mamurph@cs.clemson.edu)
#           Michael Fenn (mfenn@cs.clemson.edu)
# Revision: 26 February 2009
#
#   Copyright 2007-2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.


# Thanks to the POSIX-style getopt implementation, the main driver will use
# this syntax:
#
# stoker [stoker-options] [<target> <command> [[command-options] command-args]]

import sys, getopt, os, os.path, signal, threading, time

import scf2, stoker_resolvers
from stoker_messages import STOKER_HELP_MESSAGE, COPYRIGHT_NOTICE, \
      MSG_KNOWN_COMMANDS, MSG_USAGE_ERROR, MSG_NO_RESOLVER, MSG_EXIT_SIGNAL, \
      MSG_READ_ERROR, MSG_SIG_INT, MSG_SIG_TERM, MSG_SIG_KILL, \
      MSG_SIG_BE_PATIENT
from stoker_base import pr_error, signal_monitor
from stoker_core import Stoker, OutputHandler
from stoker_defines import EXIT_ERROR_CONFIG, EXIT_ERROR_SYNTAX, \
      STOKER_VERSION, LOCAL_CONFIG_FILENAME, USER_CONFIG_PATH, \
      SYSTEM_CONFIG_PATH, CFG_MAX_THREADS, DFLT_MAX_THREADS, EXIT_SUCCESS, \
      BUSY_WAIT_INTERVAL
from stoker_commands import pr_synopsis, STOKER_COMMAND_MAP

class StokerThread(threading.Thread):
   def __init__(self, stoker_inst, args, all_machines, stdin_path = []):
      threading.Thread.__init__(self)
      self.args = args
      self.all_machines = all_machines
      self.stoker_inst = stoker_inst
      self.stdin_path = stdin_path
   def run(self):
      self.stoker_inst.execute(self.args, self.all_machines, self.stdin_path)

def sig_alrm3(signum, frame):
   os._exit(2)

def sig_alrm2(signum, frame):
   print MSG_SIG_KILL
   signal_monitor.send_signal(signal.SIGKILL)
   signal.signal(signal.SIGALRM, sig_alrm3)
   signal.alarm(2)

def sig_alrm1(signum, frame):
   print MSG_SIG_TERM
   signal_monitor.send_signal(signal.SIGTERM)
   signal.signal(signal.SIGALRM, sig_alrm2)
   signal.alarm(2)

def sig_dummy_int(signum, frame):
   signal.signal(signal.SIGINT, sig_dummy_int)
   print MSG_SIG_BE_PATIENT

def sig_int(signum, frame):
   signal.signal(signal.SIGINT, sig_dummy_int)
   print MSG_SIG_INT
   signal_monitor.send_signal(signum)
   signal.signal(signal.SIGALRM, sig_alrm1)
   signal.alarm(5)

def show_help():
   print STOKER_HELP_MESSAGE

def main():
   arglist = sys.argv[1:]
   if arglist == []:
      show_help()
      sys.exit(EXIT_ERROR_SYNTAX)
   #
   try:
      opts, args = getopt.getopt(arglist, 'hr:lvmgpt:as:SqQci:o:e:', \
         ['help', 'reference=', 'list-commands', \
          'version', 'list-machines', 'list-groups', 'parallel',
          'threads=', 'all', 'sources=', 'list-sources', 'no-details',
          'no-summary', 'concatenate', 'input-from', 'output-to', 'error-to'])
   except getopt.GetoptError:
      show_help()
      sys.exit(EXIT_ERROR_SYNTAX)
   #
   show_short_help = False
   list_commands = False
   reference_command = ''
   show_version = False
   list_machines = False
   list_groups = False
   parallel = False
   threads = -1
   allow_execute = True
   exec_all_mode = []
   all_machines = False
   list_sources = False
   source_limit = []
   show_summary = True
   show_output = True
   raw_output = False
   
   stdin_path = []
   stdout_path = None
   stderr_path = None

   for opt, val in opts:
      if opt in ('--help', '-h'):
         show_short_help = True
         allow_execute = False
      elif opt in ('--list-commands', '-l'):
         list_commands = True
         allow_execute = False
      elif opt in ('--reference', '-r'):
         reference_command = val
         allow_execute = False
      elif opt in ('--version', '-v'):
         show_version = True
      elif opt in ('--list-machines', '-m'):
         list_machines = True
         allow_execute = False
      elif opt in ('--list-groups', '-g'):
         list_groups = True
         allow_execute = False
      elif opt in ('--parallel', '-p'):
         parallel = True
      elif opt in ('--threads', '-t'):
         threads = int(val)
         parallel = True
      elif opt in ('--list-sources', '-S'):
         list_sources = True
         allow_execute = False
      elif opt in ('--sources', '-s'):
         source_limit = [ n.strip() for n in val.split(',') ]
      elif opt in ('--all', '-a'):
         all_machines = True
      elif opt in ('--no-details', '-q'):
         show_output = False
      elif opt in ('--no-summary', '-Q'):
         show_summary = False
      elif opt in ('--concatenate', '-c'):
         raw_output = True
      elif opt in ('--input-from', '-i'):
      	stdin_path.append(val)
      elif opt in ('--output-to', '-o'):
      	stdout_path = val
      	if not os.path.isdir(stdout_path):
      	   try:
      	      tfh = open(stdout_path, 'w')
      	      tfh.close()
      	   except Exception, msg:
      	      print >> sys.stderr, msg
      	      sys.exit(2)
      elif opt in ('--error-to', '-e'):
      	stderr_path = val
      	if not os.path.isdir(stderr_path):
      	   try:
      	      tfh = open(stderr_path, 'w')
      	      tfh.close()
      	   except Exception, msg:
      	      print >> sys.stderr, msg
      	      sys.exit(2)
      else:
         print 'BUG at global option processing: got', opt

   # Do the simple stuff: print short help, long help, or version
   if show_short_help: show_help()
   if show_version:
      print 'Stoker version', STOKER_VERSION
      print COPYRIGHT_NOTICE
   #
   # Read main config
   #
   stoker_config = scf2.ConfigHandler()

   home_dir = os.getenv('HOME')
   home_conf = ''
   if os.path.exists(LOCAL_CONFIG_FILENAME):
      check = stoker_config.parseConfigFile(LOCAL_CONFIG_FILENAME)
      if not check:
         pr_error(MSG_READ_ERROR % (LOCAL_CONFIG_FILENAME), EXIT_ERROR_CONFIG)
   else:
      if home_dir != None:
         home_conf = os.path.join(home_dir, USER_CONFIG_PATH)
      if home_conf != '' and os.path.exists(home_conf):
         check = stoker_config.parseConfigFile(home_conf)
         if not check:
            pr_error(MSG_READ_ERROR % (home_conf), EXIT_ERROR_CONFIG)
      else:
         check = stoker_config.parseConfigFile(SYSTEM_CONFIG_PATH)
         if not check:
            pr_error(MSG_READ_ERROR % (SYSTEM_CONFIG_PATH), EXIT_ERROR_CONFIG)

   # Figure out how many threads to use: set to 1 for sequential (default)
   max_threads = 1
   if parallel:
      if threads > 0:
         max_threads = threads
      else:
         max_threads = int(stoker_config.getParam(CFG_MAX_THREADS, DFLT_MAX_THREADS))

   # Prepare output handler and Stoker instance
   output = OutputHandler(show_summary, stdout_path, stderr_path)
   inst = Stoker(stoker_config, max_threads, output)

   # Remove any unmatched resolvers from resolver list, if user has requested
   # this feature. Order new resolver list in order of name request.
   if len(source_limit) > 0:
      new_resolvers = []
      unavailable_sources = []
      for rqsource in source_limit:
         chk = len(new_resolvers)
         for resolver in inst.resolvers:
            if resolver.CLI_NAME == rqsource:
               new_resolvers.append(resolver)
         if chk == len(new_resolvers):   # request not matched
            unavailable_sources.append(rqsource)
      if len(unavailable_sources) > 0:
         error_list = [ MSG_NO_RESOLVER % (n) for n in unavailable_sources ]
         pr_error(error_list, EXIT_ERROR_SYNTAX)
      inst.resolvers = new_resolvers

   if list_commands:
      print MSG_KNOWN_COMMANDS
      cmds = STOKER_COMMAND_MAP.keys()
      cmds.sort()
      for cmd in cmds:
         print cmd
   if list_machines:
      for resolver in inst.resolvers:
         for machine in resolver.getAllMachines():
            print machine
   if list_groups:
      for resolver in inst.resolvers:
         for machine in resolver.getAllGroups():
            print machine
   if list_sources:
      for name in stoker_resolvers.list_resolver_names(inst.resolvers):
         print name
   if reference_command != '':
      req_commands = reference_command.split(',')
      for rqcmd in req_commands:
         pr_synopsis(rqcmd)

   # Sanity check:
   a = len(args)
   if ((not all_machines) and a < 2) or (all_machines and a < 1):
      if not allow_execute:
         pass
      elif show_version:
         allow_execute = False
      else:
         pr_error(MSG_USAGE_ERROR, EXIT_ERROR_SYNTAX)

   # If an option has not disallowed execution, perform the requested action
   if allow_execute:
      # Set up signal handler
      signal.signal(signal.SIGINT, sig_int)

      # Execute in a separate thread, so that signal handler can be used
      stoker_thread = StokerThread(inst, args, all_machines, stdin_path)
      stoker_thread.start()

      # Wait for Stoker thread to finish. This needs to be a busy wait,
      # NOT a join (otherwise, the signal handler cannot catch SIGINT)
      while stoker_thread.isAlive():
         time.sleep(BUSY_WAIT_INTERVAL)

      # Still need to print the output!
      if show_output:
         if raw_output:
            out, err = output.get_raw_results()
            if len(out) > 0:
               print out
            if len(err) > 0:
               print err
         else:
            print output
      return output.get_global_code()
   return EXIT_SUCCESS

if __name__ == '__main__':
   code = main()
   sys.exit(code)
